package game.Input;

import game.MainController;
import game.Engine.GameEngine;
import game.Engine.Entity.Alien;
import game.Engine.Entity.AlienSpawn;
import game.Engine.Entity.Entity;
import game.Engine.Entity.Player;
import game.Engine.Entity.PowerUp;
import game.Engine.Entity.Tile;
import game.Engine.Map.Map;
import game.display.Animation;
import game.display.Images;
import game.menu.Button;
import game.menu.Drawable;
import game.menu.Menu;
import game.sound.Sound;
import game.sound.SoundFilter;
import game.sound.SoundManager;
import game.util.HighscoreEntry;

import java.awt.Point;
import java.awt.event.*;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;

import javax.imageio.ImageIO;
import javax.sound.sampled.AudioFormat;

/**
 * The Engine that drives all input.
 *
 */
public class InputEngine implements KeyListener, MouseListener, MouseWheelListener, MouseMotionListener {
   /*
    * different keyCodes
    */
	public static final int MOUSE_MOVE_LEFT = 0;
	public static final int MOUSE_MOVE_RIGHT = 1;
	public static final int MOUSE_MOVE_UP = 2;
	public static final int MOUSE_MOVE_DOWN = 3;
	public static final int MOUSE_WHEEL_UP = 4;
	public static final int MOUSE_WHEEL_DOWN = 5;
	public static final int MOUSE_BUTTON_1 = 6;
	public static final int MOUSE_BUTTON_2 = 7;
	public static final int MOUSE_BUTTON_3 = 8;
	private static final int NUM_MOUSE_CODES = 9;
	
	private ArrayList<HighscoreEntry> highscores;
	private BufferedReader br;


	private HashMap<String,Sound> sounds;
	private SoundManager soundManager;
	
	// uncompressed, 44100Hz, 16-bit, mono, signed, little-endian
    private static final AudioFormat PLAYBACK_FORMAT = new AudioFormat(44100, 16, 1, true, false);
	
	// key codes are defined in java.awt.KeyEvent.
	// most of the codes (except for some rare ones like
	// "alt graph") are less than 600.
	private static final int NUM_KEY_CODES = 600;

	private int currentMap = 0;
	/**
	 * creates a new input engine.
	 */
	public InputEngine(){
	   //sets up the high scores arraylist
		highscores = new ArrayList<HighscoreEntry>();
		//sets up the sounds hashmap
		sounds = new HashMap<String,Sound>();
		//creates a new soundManager.
		soundManager = new SoundManager(PLAYBACK_FORMAT);
		//loads the highscores
		loadHighscores();
		//loads the sounds
		loadSounds();
	}
	/**
	 * returns the sounds
	 * @return the sounds
	 */
	public HashMap<String, Sound> getSounds(){
		return sounds;
	}
	/**
	 * returns the soundManager
	 * @return the soundManager
	 */
	public SoundManager getSoundManager(){
		return soundManager;
	}
	
	private void loadSounds() {
        Sound pickupSound = soundManager.getSound("/sounds/pickup.wav");
        Sound wallHitSound = soundManager.getSound("/sounds/WallHit.wav");
        Sound airProjectilInMotionSound = soundManager.getSound("/sounds/airProjectileinMotion.wav");
        Sound drowningWaterSound = soundManager.getSound("/sounds/drowningwater.wav");
        Sound fireProjectileInMotionSound = soundManager.getSound("/sounds/fireProjectileInMotion.wav");        
        Sound thundaSound = soundManager.getSound("/sounds/Thunda.wav");
        Sound waterInMotionSound = soundManager.getSound("/sounds/waterInMotion.wav");
        sounds.put("pickupSound", pickupSound);
        sounds.put("hitSound", wallHitSound);
        sounds.put("airProjectileInMotionSound", airProjectilInMotionSound);
        sounds.put("drowingWaterSound", drowningWaterSound);
        sounds.put("fireProjectileInMotionSound", fireProjectileInMotionSound);
        sounds.put("thundaSound", thundaSound);
        sounds.put("waterInMotionSound", waterInMotionSound);
	}
	/**
	 * goes and retrieves the highscores from a .dat file in the .jar file and stores them in an array
	 */
	public void loadHighscores(){
		try {
			br = new BufferedReader(new FileReader("Highscore.dat"));//Gets the high score from the current directory
			String currentLine = "";
			int count = 0;
			while ((currentLine = br.readLine()) != null && count < 10) {
				String[] parts = currentLine.split(",");
				highscores.add(new HighscoreEntry(parts[0],Integer.parseInt(parts[1])));
				count++;
			}
		} catch (FileNotFoundException e) {
			FileWriter output = null;
			try {
				output = new FileWriter("Highscore.dat");
				BufferedWriter writer = new BufferedWriter(output);
				writer.close();
			} catch (IOException ex) {
				ex.printStackTrace();
			}
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	/**
	 * returns the highscores
	 * @return the highscores
	 */
	public ArrayList<HighscoreEntry> getHighscores(){
		return highscores;
	}
	/**
	 * if the player achieves a new high score, it stores it
	 * @param name is the name of the player entered at the beginning of the game 
	 *			   so that the highscores can be kept accurately
	 * @param score is the score achieved by that current game later to be stored in
	 * 				the highscore.dat file along with the name
	 */
	public void addHighscoreEntry(String name, int score){
		highscores.add(new HighscoreEntry(name,score));
		sortHighscores();
		while(highscores.size() > 10){//makes sure there are only ten highscores stored
			highscores.remove(highscores.size()-1);
		}
		writeScoresToFile();
		Menu.setupHighscore();
	}
	
	private void writeScoresToFile(){
		FileWriter output = null;
		try {
			output = new FileWriter("Highscore.dat");
			BufferedWriter writer = new BufferedWriter(output);
			for(HighscoreEntry e: highscores){
				writer.write(e.getName()+","+e.getScore()+"\n");
			}
			writer.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * calls the collection sort method on the highscores
	 */
	public void sortHighscores(){
		Collections.sort(highscores);
	}
	
	// from the KeyListener interface
	/**
	 * Sends input to MainController.gameEngine
	 */
	public void keyPressed(KeyEvent e) {
		char c = Character.toLowerCase(e.getKeyChar());
		switch (c) {
		case 'w':
			sendInput(GameAction.PLAYER_UP); //Jump is mapped to 'w'
			break;
		case 'a':
			sendInput(GameAction.PLAYER_LEFT); //Move left with 'a'
			break;
		case 's':
			sendInput(GameAction.PLAYER_DOWN); //"Slide" or crouch if stationary with 's'
			break;
		case 'd':
			sendInput(GameAction.PLAYER_RIGHT); //Move right with 'd'
			break;
		case '1':
			sendInput(GameAction.PLAYER_MAGIC_WHITE); //Change player color and magic to white with '1'
			break;
		case '2':
			sendInput(GameAction.PLAYER_MAGIC_RED); //Change player color and magic to red with '2'
			break;
		case '3':
			sendInput(GameAction.PLAYER_MAGIC_GREEN); //Change player color and magic to green with '3'
			break;
		case '4':
			sendInput(GameAction.PLAYER_MAGIC_BLUE); //Change player color and magic to blue with '4'
			break;
		case '5':
			sendInput(GameAction.PLAYER_MAGIC_YELLOW); //Change player color and magic to yellow with '5'
			break;
		case 'p':
			MainController.gameEngine.paused = !MainController.gameEngine.paused;
			break;
		}
		if(e.getKeyCode() == KeyEvent.VK_ESCAPE) {
			MainController.soundController.stop();
			MainController.stop();
			System.exit(0);
		}
		//e.consume();
	}
	// from the KeyListener interface
	/**
	 * Sends input to MainController.gameEngine
	 */
	public void keyReleased(KeyEvent e) {
		char c = Character.toLowerCase(e.getKeyChar());
		switch (c) {
		case 'w':
			sendInput(GameAction.PLAYER_UP_R);
			break;
		case 'a':
			sendInput(GameAction.PLAYER_LEFT_R);
			break;
		case 's':
			sendInput(GameAction.PLAYER_DOWN_R);
			break;
		case 'd':
			sendInput(GameAction.PLAYER_RIGHT_R);
			break;
		}
		//e.consume();
	}
	/**
	 * @param a is a gameAction that is sent to MainController.gameEngine
	 */
	public void sendInput(GameAction a) {
		MainController.gameEngine.addGameAction(a);
	}

	// from the KeyListener interface
	public void keyTyped(KeyEvent e) {
		// make sure the key isn't processed for anything else
		//e.consume();
	}

	// from the MouseListener interface
	/**
	 * based on the state of the game, it either sends the signal to cast magic, or
	 * determines where the user clicks, changes the state of the game, and displays
	 * the appropriate screens 
	 */
	public void mousePressed(MouseEvent e) {
		if(MainController.getState() != MainController.State.GAME) {// sees if in game or in menu state
			HashSet<Drawable> drawables = MainController.getCurrentMenu().drawables;//gets the drawable things from mainController
			for(Drawable d: drawables){//goes through the drawable collection
				Button b = null;
				if(d instanceof Button)b = (Button)d;//creates a button on screen if thing from collection was button
				if(b != null && b.containsPoint(e.getPoint())){

					if(b.getMenuAction() == MenuAction.SINGLE_PLAYER) {//if clicked on single-player, sets state, makes cursor invisible, starts game
						MainController.j.setCursor(MainController.invisibleCursor);
						MainController.gameEngine = new GameEngine(MainController.nextMap());
						MainController.displayEngine.displayGame(MainController.gameEngine);
						MainController.gameEngine.start();
						MainController.soundController.startMidiPlayer();
					}
					else if(b.getMenuAction() == MenuAction.TUTORIAL){//if clicked on tutorial, displays the tutorial screen
						MainController.setState(MainController.State.TUTORIAL_MENU);
						MainController.displayEngine.displayMenu(Menu.tutorialMenu);
					}else if(b.getMenuAction() == MenuAction.BACK){//if clicked on back, takes the user to the main menu
						MainController.setState(MainController.State.MENU);
						MainController.displayEngine.displayMenu(Menu.mainMenu);
					}else {//if highscores is clicked, takes the user to the highscores screen
						MainController.setState(MainController.State.HIGHSCORE_MENU);
						MainController.displayEngine.displayMenu(Menu.highscoreMenu);
						MainController.j.repaint();
						break;
					}
				}
			}
		}else sendInput(GameAction.PLAYER_CAST);//sends input that the player is casting a spell
	}

	// from the MouseListener interface
	/**
	 * sends input that the player released the click of cast
	 */
	public void mouseReleased(MouseEvent e) {
		sendInput(GameAction.PLAYER_CAST_R);
	}

	// from the MouseListener interface
	public void mouseClicked(MouseEvent e) {
	}
	
	/**
	 * returns a new animation with each frame have length of 150 milliseconds
	 * @param s is the name of the file
	 * @returns a new animation with each frame have length of 150 milliseconds
	 */
	public static Animation loadImage(String s) {
		BufferedImage i = null;
		try {
			i = ImageIO.read(InputEngine.class.getResourceAsStream("/images/"+s+".png"));
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return new Animation().addFrame(i,150);
	}

	/**
	 * returns a new animation with each frame having the length of l
	 * @param s is an array with the names of the pictures
	 * @param l is the length of the animation frame in nanoseconds
	 * @return a new animation with each frame having the length of l
	 */
	public static Animation loadAnimation(String[] s, long[] l) {
		Animation a = new Animation();
		for (int i = 0; i < s.length; i++) {
			try {
				a.addFrame(ImageIO.read(InputEngine.class.getResourceAsStream("/images/"+s[i]+".png")), l[i]);
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		return a;
	}

	/**
	 * returns a new animation
	 * @param s is an array with the names of the pictures
	 * @param j is the length of the animation frame
	 * @return a new animation
	 */
	public static Animation loadAnimation(String[] s,long j) {
		Animation a = new Animation();
		for (int i = 0; i < s.length; i++) {
			try {
				a.addFrame(ImageIO.read(InputEngine.class.getResourceAsStream("/images/"+s[i]+".png")), j);
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		return a;
	}
	
	/**
	 * returns a new animation
	 * @param s is an array containing the names of the pictures
	 * @param l is the length of each frame of the animation in nanoseconds
	 * @param repeats determins whether the animation should repeat or not
	 * @return a new animation
	 */

	public static Animation loadAnimation(String[] s, long[] l, boolean repeats) {
		Animation a = new Animation();
		for (int i = 0; i < s.length; i++) {
			try {
				a.addFrame(ImageIO.read(InputEngine.class.getResourceAsStream("/images/"+s[i]+".png")), l[i]);
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		return a.setRepeats(repeats);
	}

	@Override
	public void mouseEntered(MouseEvent e) {
		//Nothing
	}

	@Override
	public void mouseExited(MouseEvent e) {
		// nothing

	}

	// ************* Loading of Map *****************
	public Map loadMap(String fileName) throws IOException {
		ArrayList<String> lines = new ArrayList<String>();
		int width = 0;
		int height = 0;

		// Reads the lines of the text file into the list
		BufferedReader reader = new BufferedReader(new InputStreamReader(getClass().getResourceAsStream(fileName)));
		while (true) {
			String line = reader.readLine();

			// No more lines to read
			if (line == null) {
				reader.close();
				break;
			}

			// Add every line except the comments
			if (!line.startsWith("#")) {
				lines.add(line);
				width = Math.max(width, line.length());
			}
		}

		// Parse the lines
		height = lines.size();
		Map map = new Map(width, height);
		for (int y = 0; y < height; y++) {
			String line = (String) lines.get(y);
			for (int x = 0; x < line.length(); x++) {
				char ch = line.charAt(x);
				Entity entity = getEntity(ch, x, y);
				if (entity != null) {
					map.putEntity(x, y, entity);
				}
			}
		}
		return map;
	}

	private Entity getEntity(char ch, int x, int y) {
		if (ch == 'A' || ch == 'B' || ch == 'C' || ch == 'D' || ch == 'E'
				|| ch == 'F' || ch == 'G' || ch == 'H' || ch == 'I'
				|| ch == 'J' || ch =='K'  || ch == 'L' || ch == 'M' || ch == 'N') {
			return new Tile(new Point(x, y), ch);
		} else if (ch == 'P') {
			return new Player(new Point2D.Double(x, y));
		} else if (ch == 'o') {

		} else if (ch == '!') {

		} else if (ch == '*') {
			return new PowerUp(new Point(x,y),(int) (Math.random()*6));
		} else if (ch == '1') {
			return new AlienSpawn(x, y, Images.SPAWNPOINT.image());
		} else if (ch == '2') {
			return new AlienSpawn(x, y, Images.SPAWNPOINT.image());
		}
		return null;
	}

	/**
	 * returns the next map
	 * @return the next map
	 */
	public Map loadNextMap() {
		Map map = null;
		while (map == null) {
		   currentMap = (int) (Math.random() * 3) + 1;
			try {
				map = loadMap("/maps/map" + currentMap + ".txt");
			} catch (IOException ex) {
				if (currentMap == 1) {
					// no maps to load!
					return null;
				}
				currentMap = 0;
				map = null;
			}
		}
		return map;
	}

	/**
	 * reloads the map that was just loaded
	 * @return the map that was just loaded
	 */
	public Map reloadMap() {
		try {
			return loadMap("/maps/map" + currentMap + ".txt");
		} catch (IOException ex) {
			ex.printStackTrace();
			return null;
		}
	}

	/**
	 * sends input to the GameEngine that the mouse wheel has been rotated
	 */
	public void mouseWheelMoved(MouseWheelEvent arg0) {
		if (arg0.getWheelRotation() > 0)
			sendInput(GameAction.PLAYER_WHEEL_SCROLL_UP);
		else
			sendInput(GameAction.PLAYER_WHEEL_SCROLL_DOWN);
	}

	@Override
	public void mouseDragged(MouseEvent arg0) {

	}

	@Override
	public void mouseMoved(MouseEvent me) {
		//Checks to see if the cursor is in the button and changes the button to be highlighted
		if(MainController.getState() != MainController.State.GAME){
			for(Drawable d: MainController.getCurrentMenu().drawables){
				Button b = null;
				if(d instanceof Button){
					b = (Button)d;
					if(b.containsPoint(me.getPoint()))b.setHighlighted(true);
					else b.setHighlighted(false);	
				}
			}
		}
		MainController.j.repaint();
	}
}